using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Linq;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Magiq.Databases;
using Magiq.Databases.Delete;
using Magiq.Databases.Insert;
using Magiq.Databases.Query;
using Magiq.Databases.SqlServer;
using Magiq.Databases.Update;
using Magiq.Delete;
using Magiq.Insert;
using Magiq.Query;
using Magiq.Sql.Insert;
using Magiq.Sql.Support;
using Magiq.Support;
using Magiq.Update;

namespace Magiq.Sql
{
    public class LinqToSqlDatabaseAdapter : IDatabaseAdapter
    {
        public static readonly SqlServerDialect SqlServerDialect = new SqlServerDialect();

        private readonly DataContext dataContext;
        private readonly MagiqToSqlQueryableFactory queryableFactory = new MagiqToSqlQueryableFactory();

        public LinqToSqlDatabaseAdapter(DataContext dataContext)
        {
            this.dataContext = dataContext;
        }

        public IDbCommand GetCommand(IQueryable queryable)
        {
            return dataContext.GetCommand(queryable);
        }

        public ITypeMapping GetMappingOf(Type type)
        {
            return new SqlTypeMapping(dataContext.Mapping.GetMetaType(type));
        }

        public IDbCommand GetCommand()
        {
            return dataContext.Connection.CreateCommand();
        }

        public IDialect Dialect { get { return SqlServerDialect; } }

        public IEnumerable<DatabaseDelete<T>> BuildDatabaseDeletes<T>(IDelete<T> delete) where T : class
        {
            yield return new DatabaseDelete<T>(this)
                             {
                                 Source = delete.Source
                             };
        }

        public IEnumerable<DatabaseUpdate<T>> BuildDatabasUpdates<T>(IUpdate<T> update) where T : class
        {
            var emulator = new MultipleTableUpdateEmulation<T>(update, this);
            return emulator.GetTableUpdates();
        }

        public IEnumerable<DatabaseInsert<TSource>> BuildDatabaseInserts<TSource, TDestination, TNew>(IInsert<TSource, TDestination, TNew> insert)
        {
            var mapping = GetMappingOf(typeof(TNew));
            var newExpression = (MemberInitExpression)insert.NewExpression.Body;

            var databaseInsert = new DatabaseInsert<TSource>
                                     {
                                         DatabaseAdapter = this,
                                         DestinationTypeMapping = mapping,
                                         Source = insert.Source
                                     };

            foreach (MemberAssignment binding in newExpression.Bindings)
            {
                var parameter = binding.Expression.IsConstant()
                                    ? Expression.Parameter(newExpression.Type, "x")
                                    : binding.Expression.GetParameter();

                var valueExpression = Expression.Lambda(binding.Expression, parameter);
                var property = (PropertyInfo)binding.Member;

                var assignment = new InsertColumnsAssignment
                                     {
                                         Columns = mapping.ColumnsFor(property.Name),
                                         PropertyName = property.Name,
                                         PropertyType = property.PropertyType,
                                         ValueExpression = valueExpression,
                                         ValueType = property.PropertyType
                                     };

                databaseInsert.ColumnAssignments.Add(assignment);
            }
         
            //inheritance
            var destinationInheritance = new TypeInheritanceMapping(newExpression.Type, dataContext);

            if (destinationInheritance.IsInheritance)
            {
                var discriminatorProperty = destinationInheritance.DiscriminatorProperty;
                var valueExpression = Expression.Lambda(Expression.Constant(destinationInheritance.DiscriminatorCode),
                                                        Expression.Parameter(destinationInheritance.Type, "x"));

                databaseInsert.ColumnAssignments.Add(new InsertColumnsAssignment
                                                         {
                                                             Columns = mapping.ColumnsFor(discriminatorProperty.Name),
                                                             PropertyName = discriminatorProperty.Name,
                                                             PropertyType = discriminatorProperty.PropertyType,
                                                             ValueExpression = valueExpression,
                                                             ValueType = discriminatorProperty.PropertyType
                                                         });
            }
         
         //Collections
         EntitySetAdapter entitySetAdapter = null;

         if (insert.Destination is IEntitySetAdapterProvider)
             entitySetAdapter = ((IEntitySetAdapterProvider)insert.Destination).EntitySetAdapter;
         else if (insert.Destination.IsDeferredEntitySet())
             entitySetAdapter = new EntitySetAdapter(insert.Destination);

         if (entitySetAdapter != null)
         {
             var member = entitySetAdapter.Member;

             for (var i = 0; i < member.Association.ThisKey.Count; i++)
             {
                 var parentKeyValue = entitySetAdapter.Instance.Property(member.Association.ThisKey[i].Name);
                 var valueExpression = Expression.Lambda(Expression.Constant(parentKeyValue),
                                                         Expression.Parameter(typeof (TNew), "x"));
                 var childKeyProperty = (PropertyInfo) member.Association.OtherKey[i].Member;

                 databaseInsert.ColumnAssignments.Add(new InsertColumnsAssignment
                                                          {
                                                              Columns = mapping.ColumnsFor(childKeyProperty.Name),
                                                              PropertyName = childKeyProperty.Name,
                                                              PropertyType = childKeyProperty.PropertyType,
                                                              ValueExpression = valueExpression,
                                                              ValueType = childKeyProperty.PropertyType
                                                          });
             }
         }
     
            yield return databaseInsert;
        }

        public IInsertDestination<TNew> GetInsertDestination<TSource, TDestination, TNew>(IInsert<TSource, TDestination, TNew> insert)
            where TDestination : class
            where TNew : class, TDestination, new()
        {
            var destination = insert.Destination;

            if (destination.IsTable())
                return new InsertTable<TNew>((ITable)destination, this);

            if (destination is IEntitySetAdapterProvider || destination.IsDeferredEntitySet())
                return new EntitySetInsertDestination<TDestination,TNew>((EntitySet<TDestination>) destination);

            return null;
        }

        public DatabaseQuery<T> BuildDatabaseQuery<T>(IQuery<T> query)
        {
            return new DatabaseQuery<T>
                       {
                           DatabaseAdapter = this,
                           SourceTypeMapping = GetMappingOf(typeof (T)),
                           Source = query.Source
                       };
        }

        public IQueryable<T> GetQueryable<T>(DatabaseQuery<T> databaseQuery) where T : class
        {
            if (databaseQuery.Source.IsDeferredEntitySet())
                return queryableFactory.GetFrom((EntitySet<T>)databaseQuery.Source);
            
            return databaseQuery.Source.AsQueryable();
        }

        public IDbConnection GetConnection()
        {
            return dataContext.Connection;
        }
    }
}